local _G = _G
local CurseProfiler = _G.CurseProfiler
local PlayerProfile = CurseProfiler:NewModule("PlayerProfile")

local ipairs = _G.ipairs
local pairs = _G.pairs
local select = _G.select
local math_floor = _G.math.floor
local math_max = _G.math.max
local GetNumSpellTabs = _G.GetNumSpellTabs
local GetSpellTabInfo = _G.GetSpellTabInfo
local GetSpellInfo = _G.GetSpellInfo
local GetSpellLink = _G.GetSpellLink
local BOOKTYPE_SPELL = _G.BOOKTYPE_SPELL
local GetTradeSkillRecipeLink = _G.GetTradeSkillRecipeLink
local GetTradeSkillItemLink = _G.GetTradeSkillItemLink
local GetNumTradeSkills = _G.GetNumTradeSkills
local GetTradeSkillLine = _G.GetTradeSkillLine
local GetCraftRecipeLink = _G.GetCraftRecipeLink
local GetCraftItemLink = _G.GetCraftItemLink
local GetNumCrafts = _G.GetNumCrafts
local GetCraftSkillLine = _G.GetCraftSkillLine

local function getSpellIDFromName(name)
	for i = 1, GetNumSpellTabs() do
		local _, _, offset, numSpells = GetSpellTabInfo(i)
		for j = 1, numSpells do
			local skillID = j + offset
			if GetSpellInfo(skillID, BOOKTYPE_SPELL) == name then
				local link = GetSpellLink(skillID, BOOKTYPE_SPELL)
				return link:match("^|c%x%x%x%x%x%x%x%x|Hspell:(%d+)|h%[.-%]|h|r$")+0
			end
		end
	end
	return nil
end

local function getNameFromSpellID(spellID)
	local name = GetSpellInfo(spellID)
	if getSpellIDFromName(name) ~= spellID then
		return nil
	end
	return name
end

local professions = {}
local function updateProfessions(name, number, itemLinkFunc, recipeLinkFunc)
	if not name then
		return
	end
	local professions_name = professions[name]
	if professions_name then
		for k in pairs(professions_name) do
			professions_name[k] = nil
		end
	else
		professions_name = {}
		professions[name] = professions_name
	end
	for i = 1, number do
		local recipeLink = recipeLinkFunc(i)
		local itemLink = itemLinkFunc(i)
		if recipeLink and itemLink then
			local recipeID = recipeLink:match("^|c%x%x%x%x%x%x%x%x|Henchant:(%d+)|h%[.-%]|h|r$")+0
			
			local enchantID = itemLink:match("^|c%x%x%x%x%x%x%x%x|Henchant:(%d+)|h%[.-%]|h|r$")
			local itemID
			if enchantID then
				enchantID = enchantID+0
				professions_name[{ recipeID, enchantID, true }] = true
			else	
				itemID = itemLink:match("^|c%x%x%x%x%x%x%x%x|Hitem:(%d+):[%-%d:]*|h%[.-%]|h|r$")+0
				professions_name[{ recipeID, itemID, false }] = true
			end
		end
	end
end

function PlayerProfile:CRAFT_SHOW()
	updateProfessions(GetCraftSkillLine(1), GetNumCrafts(), GetCraftItemLink, GetCraftRecipeLink)
end

function PlayerProfile:TRADE_SKILL_SHOW()
	updateProfessions(GetTradeSkillLine(1), GetNumTradeSkills(), GetTradeSkillItemLink, GetTradeSkillRecipeLink)
end

local recentItems = {}
local matches = {
	"^" .. LOOT_ITEM_CREATED_SELF:gsub("%%s.*", ""),
	"^" .. LOOT_ITEM_CREATED_SELF_MULTIPLE:gsub("%%s.*", ""),
	"^" .. LOOT_ITEM_PUSHED_SELF:gsub("%%s.*", ""),
	"^" .. LOOT_ITEM_PUSHED_SELF_MULTIPLE:gsub("%%s.*", ""),
	"^" .. LOOT_ITEM_SELF:gsub("%%s.*", ""),
	"^" .. LOOT_ITEM_SELF_MULTIPLE:gsub("%%s.*", ""),
}
function PlayerProfile:CHAT_MSG_LOOT(event, text)
	local item = text:match("(item:%d+:%-?%d+:%-?%d+:%-?%d+:%-?%d+:%-?%d+:%-?%d+:%-?%d+:%-?%d+)")
	if item then
		local good = false
		for i, match in ipairs(matches) do
			if text:match(match) then
				good = true
				break
			end
		end
		if good then
			local _, _, quality = GetItemInfo(item)
			local itemID = item:match("^item:(%d+)")
			if quality >= 3 then -- blues or higher
				table.insert(recentItems, 1, itemID+0)
				for i = #recentItems, 101, -1 do
					recentItems[i] = nil
				end
			end
		end
	end
end

local safeSkills = {}
function PlayerProfile:PLAYER_LOGIN()
	for i = 1, _G.GetNumSkillLines() do
		local name, isHeader, _, rank,  _, _, max = _G.GetSkillLineInfo(i)
		if not isHeader then
			safeSkills[{ name or '', rank or 0, max or 0 }] = true
		end
	end
end

local otherChars = {}

function PlayerProfile:OnSerialize()
	local INT = CurseProfiler.makeSerializedInteger
	local TUPLE = CurseProfiler.makeSerializedTuple
	local RAW = CurseProfiler.makeRaw
	local ITEM = CurseProfiler.makeSerializedItem
	local ENUM = CurseProfiler.makeSerializedEnum
	local FACTION = CurseProfiler.getFactionID
	
	local fullData = {}
	local data = {}
	data[#data+1] = _G.UnitName("player") or '' -- name
	data[#data+1] = (_G.GetRealmName() or ''):gsub("^ ", ""):gsub(" $", "") -- realm
	data[#data+1] = ENUM('race', select(2, _G.UnitRace("player"))) -- race
	data[#data+1] = ENUM('class', select(2, _G.UnitClass("player"))) -- class
	data[#data+1] = INT("Byte", _G.UnitLevel("player") or 0) -- level
	data[#data+1] = INT("Int32", _G.UnitHealthMax("player") or 0) -- health
	data[#data+1] = INT("Int32", _G.UnitManaMax("player") or 0) -- mana
	data[#data+1] = INT("Int32", _G.UnitXP("player") or 0) -- xp
	data[#data+1] = INT("Int32", _G.UnitXPMax("player") or 0) -- max xp
	data[#data+1] = _G.GetGuildInfo("player") or '' -- guild name
	data[#data+1] = select(2, _G.GetGuildInfo("player")) or '' -- guild rank
	data[#data+1] = INT("Byte", _G.UnitSex("player") or 0) -- gender
	data[#data+1] = INT("Int64", _G.GetMoney("player") or 0) -- copper
	
	local tmp, tmp2 = {}, {}
	local talents = {}
	data[#data+1] = talents
	for i = 1, _G.GetNumTalentTabs() do
		for j = 1, _G.GetNumTalents(i) do
			local link = _G.GetTalentLink(i, j)
			local talentID, rank = link:match("^|c%x%x%x%x%x%x%x%x|Htalent:(%d+):(%-?%d+)|h%[.-%]|h|r$")
			talentID = talentID+0
			rank = rank+1
			if rank > 0 then
				talents[TUPLE {
					INT("Int32", talentID),
					INT("Byte", rank)
				}] = true
			end
		end
	end
	
	local spells = {}
	data[#data+1] = spells
	for i = 1, GetNumSpellTabs() do
		local _, _, offset, numSpells = GetSpellTabInfo(i)
		for j = 1, numSpells do
			local skillID = j + offset
			local link = GetSpellLink(skillID, BOOKTYPE_SPELL)
			local spellID = link:match("^|c%x%x%x%x%x%x%x%x|Hspell:(%d+)|h%[.-%]|h|r$")+0
			spells[INT("Int32", spellID)] = true
		end
	end
	
	local factions = {}
	local rep_list = {}
	local numfactions = _G.GetNumFactions()
	-- Lets expand all the headers
	for i = numfactions, 1, -1 do
		local name, _, _, _, _, _, _, _, _, isCollapsed = GetFactionInfo(i)
		if (isCollapsed) then
			ExpandFactionHeader(i)
			rep_list[name] = true
		end
	end
	-- Get the rep levels
	for i = 1, numfactions, 1 do
		local name, _, standing, _, _, value, atwar, _, header = _G.GetFactionInfo(i)
		if not header then
			factions[TUPLE { FACTION(name or ''), INT("Byte", standing), INT("Int32", value), not not atwar }] = true
		end
	end
	-- Collapse the headers again
	for i = numfactions, 1, -1 do
		local name = GetFactionInfo(i)
		if (rep_list[name]) then
			CollapseFactionHeader(i)
		end
	end
	data[#data+1] = factions
	
	local friends = {}
	data[#data+1] = friends
	for i = 1, _G.GetNumFriends() do
		local name = _G.GetFriendInfo(i)
		if name then
			friends[name] = true
		end
	end
	
	local skills = {}
	data[#data+1] = skills
	local bad = false
	for i = 1, _G.GetNumSkillLines() do
		local name, isHeader, _, rank,  _, _, max = _G.GetSkillLineInfo(i)
		if not isHeader then
			if not name or name == '' then
				bad = true
				break
			end
			skills[TUPLE { name or '', INT("Int16", rank or 0), INT("Int16", max or 0) }] = true
		end
	end
	if bad then
		for t in pairs(skills) do
			skills[k] = nil
		end
		for t in pairs(safeSkills) do
			skills[TUPLE { t[1], INT("Int16", t[2]), INT("Int16", t[3]) }] = true
		end
	end
	
	local profession_data = {}
	data[#data+1] = profession_data
	for name, data in pairs(professions) do
		local spellID = getSpellIDFromName(name)
		if spellID then
			local t = {}
			profession_data[INT("Int32", spellID)] = t
			for k in pairs(data) do
				t[TUPLE { INT("Int32", k[1]), INT("Int32", k[2]), k[3] }] = true
			end
		end
	end

	for i = 1, 24 do
		local rating = _G.GetCombatRating(i)
		if rating < 0 then
			rating = 0
		end
		local percent = math_floor(_G.GetCombatRatingBonus(i) * 2^24 + 0.5)
		data[#data+1] = INT("Int16", rating)
	end
	
	for i = 1, 5 do
		local base, _, posBuff, negBuff = _G.UnitStat("player", i)
		data[#data+1] = INT("Int16", base)
		data[#data+1] = INT("Int16", posBuff - negBuff)
	end
	
	for i = 0, 6 do 
		local base, _, posBuff, negBuff = _G.UnitResistance("player", i)
		data[#data+1] = INT("Int16", base)
		data[#data+1] = INT("Int16", posBuff - negBuff)
	end

	local mhBase, mhMod, ohBase, ohMod = _G.UnitAttackBothHands("player")
	local mhWeaponSkill = mhBase + mhMod
	local ohWeaponSkill = ohBase + ohMod
	data[#data+1] = INT("Int16", mhWeaponSkill)
	data[#data+1] = INT("Int16", ohWeaponSkill)
	
	local mhMinDamage, mhMaxDamage, ohMinDamage, ohMaxDamage = _G.UnitDamage("player")
	data[#data+1] = INT("Int16", math_max(math_floor(mhMinDamage+0.5), 1))
	data[#data+1] = INT("Int16", math_max(math_floor(mhMaxDamage+0.5), 1))
	if ohMinDamage == 0 then
		data[#data+1] = INT("Int16", 0)
		data[#data+1] = INT("Int16", 0)
	else
		data[#data+1] = INT("Int16", math_max(math_floor(ohMinDamage+0.5), 1))
		data[#data+1] = INT("Int16", math_max(math_floor(ohMaxDamage+0.5), 1))
	end
	
	local mhSpeed, ohSpeed = _G.UnitAttackSpeed("player")
	data[#data+1] = INT("Int16", math_floor(mhSpeed*100 + 0.5))
	data[#data+1] = INT("Int16", math_floor((ohSpeed or 0)*100 + 0.5))
	
	local base, posBuff, negBuff = _G.UnitAttackPower("player")
	data[#data+1] = INT("Int16", base)
	data[#data+1] = INT("Int16", posBuff + negBuff)
	
	data[#data+1] = INT("Int16", math_floor((_G.GetCritChance() or 0)*100 + 0.5))
	
	local hasRanged = _G.GetInventoryItemTexture("player", 18) and not _G.UnitHasRelicSlot("player")
	local raBase, raMod = _G.UnitRangedAttack("player")
	local rangedSkill = hasRanged and raBase + raMod or 0
	
	data[#data+1] = INT("Int16", rangedSkill)
	
	local rangedAttackSpeed, minDamage, maxDamage = _G.UnitRangedDamage("player")
	data[#data+1] = INT("Int16", hasRanged and math_max(math_floor(minDamage+0.5), 1) or 0)
	data[#data+1] = INT("Int16", hasRanged and math_max(math_floor(maxDamage+0.5), 1) or 0)
	
	data[#data+1] = INT("Int16", math_floor(rangedAttackSpeed*100 + 0.5))
	
	local base, posBuff, negBuff = _G.UnitRangedAttackPower("player")
	data[#data+1] = INT("Int16", base)
	data[#data+1] = INT("Int16", posBuff + negBuff)
	
	data[#data+1] = INT("Int16", math_floor((_G.GetRangedCritChance() or 0)*100 + 0.5))
	
	for i = 2, 7 do
		data[#data+1] = INT("Int16", _G.GetSpellBonusDamage(i))
	end
	
	data[#data+1] = INT("Int16", _G.GetSpellBonusHealing())
	
	for i = 2, 7 do
		data[#data+1] = INT("Int16", math_floor((_G.GetSpellCritChance(i) or 0)*100 + 0.5))
	end
	
	data[#data+1] = INT("Int16", _G.GetSpellPenetration())
	
	local base, casting = _G.GetManaRegen()
	if _G.UnitHasMana("player") then
		data[#data+1] = INT("Int16", math_floor(base * 5 + 0.5))
		data[#data+1] = INT("Int16", math_floor(casting * 5 + 0.5))
	else
		data[#data+1] = INT("Int16", 0)
		data[#data+1] = INT("Int16", 0)
	end
	
	local base, modifier = _G.UnitDefense("player")
	data[#data+1] = INT("Int16", base)
	data[#data+1] = INT("Int16", modifier)
	
	data[#data+1] = INT("Int16", math_floor(_G.GetDodgeChance() * 100 + 0.5))
	data[#data+1] = INT("Int16", math_floor(_G.GetParryChance() * 100 + 0.5))
	data[#data+1] = INT("Int16", math_floor(_G.GetBlockChance() * 100 + 0.5))
	
	for i, slot in ipairs({ "HeadSlot", "NeckSlot", "ShoulderSlot", "BackSlot", "ChestSlot", "ShirtSlot", "TabardSlot", "WristSlot", "HandsSlot", "WaistSlot", "LegsSlot", "FeetSlot", "Finger0Slot", "Finger1Slot", "Trinket0Slot", "Trinket1Slot", "MainHandSlot", "SecondaryHandSlot", "RangedSlot", "AmmoSlot", }) do
		local num = _G.GetInventorySlotInfo(slot)
		local itemLink = _G.GetInventoryItemLink("player", num)
		data[#data+1] = ITEM(itemLink or nil)
	end
	
	local items = {}
	data[#data+1] = items
	for i, v in ipairs(recentItems) do
		items[#items+1] = INT("Int32", v)
	end
	
	for _, companion_type in ipairs({ "MOUNT", "CRITTER" }) do
		local companions = {}
		data[#data+1] = companions
		for i = 1, GetNumCompanions(companion_type) do
			local _, _, creatureSpellID = GetCompanionInfo(companion_type, i)
			companions[#companions+1] = INT("Int32", creatureSpellID)
		end
	end
	
	local achievements = {}
	data[#data+1] = achievements
	for _, category_id in ipairs(GetCategoryList()) do
		local numAchievements, numCompleted = GetCategoryNumAchievements(category_id)
		
		for i = 1, numAchievements do
			local achievementID, _, _, isCompleted = GetAchievementInfo(category_id, i)
			if isCompleted then
				achievements[#achievements+1] = INT("Int32", achievementID)
			end
		end
	end
	
	fullData[#fullData+1] = TUPLE(data)
	for i, v in ipairs(otherChars) do
		fullData[#fullData+1] = RAW(v, 1)
	end
	return fullData
end

function PlayerProfile:OnDeserialize(text, position)
	local deserialize = CurseProfiler.deserialize
	
	local numChars
	position, numChars = deserialize("list", text, position)
	for _ = 1, numChars do
		local length
		local startPosition = position
		local name, realm
		position, name = deserialize("string", text, position)
		position, realm = deserialize("string", text, position)
		local isCurrent = name == _G.UnitName("player") and realm == _G.GetRealmName()
		position = deserialize("enum-race", text, position, true)
		position = deserialize("enum-class", text, position, true)
		position = deserialize("Byte", text, position, true)
		position = deserialize("UInt32", text, position, true)
		position = deserialize("UInt32", text, position, true)
		position = deserialize("UInt32", text, position, true)
		position = deserialize("UInt32", text, position, true)
		position = deserialize("string", text, position, true)
		position = deserialize("string", text, position, true)
		position = deserialize("Byte", text, position, true)
		position = deserialize("Int64", text, position, true)
	
		local length
		-- talents
		position, length = deserialize("set", text, position)
		for i = 1, length do
			position = deserialize("Int32", text, position, true)
			position = deserialize("Byte", text, position, true)
		end
	
		-- spells
		position, length = deserialize("set", text, position)
		for i = 1, length do
			position = deserialize("Int32", text, position, true)
		end
	
		-- factions
		position, length = deserialize("set", text, position)
		for i = 1, length do
			position = deserialize("faction", text, position, true)
			position = deserialize("Byte", text, position, true)
			position = deserialize("Int32", text, position, true)
			position = deserialize("boolean", text, position, true)
		end
	
		-- friends
		position, length = deserialize("set", text, position)
		for i = 1, length do
			position = deserialize("string", text, position, true)
		end
	
		-- skills
		position, length = deserialize("set", text, position)
		for i = 1, length do
			position = deserialize("string", text, position, true)
			position = deserialize("Int16", text, position, true)
			position = deserialize("Int16", text, position, true)
		end
	
		-- professions
		position, length = deserialize("dict", text, position)
		for i = 1, length do
			local spellID, name
			position, spellID = deserialize("Int32", text, position)
			
			local name = getNameFromSpellID(spellID)
			
			local len
			position, len = deserialize("list", text, position)
			
			local professions_name = {}
			if name and isCurrent then
				professions[name] = professions_name
			end
			for j = 1, len do
				local recipeID, itemID, isEnchant
			
				position, recipeID = deserialize("Int32", text, position)
				position, itemID = deserialize("Int32", text, position)
				position, isEnchant = deserialize("boolean", text, position)
				professions_name[{ recipeID, itemID, isEnchant }] = true
			end
		end
		
		for i = 1, 24 do
			position = deserialize("Int16", text, position, true)
		end
		
		for i = 1, 12 do
			position = deserialize("Int16", text, position, true)
			position = deserialize("Int16", text, position, true)
		end
	
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
	
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
	
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
	
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
	
		position = deserialize("Int16", text, position, true)
	
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
	
		for i = 1, 6 do
			position = deserialize("Int16", text, position, true)
		end
		position = deserialize("Int16", text, position, true)
		for i = 1, 6 do
			position = deserialize("Int16", text, position, true)
		end
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
	
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		position = deserialize("Int16", text, position, true)
		
		for i = 1, 20 do
			position = deserialize("item", text, position, true)
		end
		
		local items
		position, items = deserialize("list", text, position)
		for i = 1, items do
			local item
			position, item = deserialize("Int32", text, position, not isCurrent)
			if isCurrent then
				recentItems[#recentItems] = item
			end
		end
		
		for _ = 1, 2 do -- MOUNT and CRITTER
			local companions
			position, companions = deserialize("list", text, position)
			for i = 1, companions do
				position = deserialize("Int32", text, position, true)
			end
		end
		
		local achievements
		position, achievements = deserialize("list", text, position)
		for i = 1, achievements do
			position = deserialize("Int32", text, position, true)
		end
		
		if not isCurrent then
			otherChars[#otherChars+1] = text:sub(startPosition, position-1)
		end
	end
	return position
end
